package edu.cqie.smartfarm.exception;


import edu.cqie.smartfarm.exception.annotation.GlobalException;
import edu.cqie.smartfarm.exception.exception.GlobalExceptionProcess;
import edu.cqie.smartfarm.exception.exception.IGlobalException;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ExceptionDepthComparator;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;


/**
 * Global exception handling class
 *
 * @author shinetan
 * @since 0.1.0
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    //扫描基础包
    private final String scanBasePackage;

    //排除的异常类
    private final List<String> exclusionDefaultGlobalException;

    private final Map<Class<? extends Throwable>, IGlobalException> mappedExceptions = new HashMap<>(16);

    private final Map<Class<? extends Throwable>, IGlobalException> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);

    public GlobalExceptionHandler(@Value("${global.exception.exclusionDefaultGlobalException:}") List<String> exclusionDefaultGlobalException, @Value("${global.exception.scanBasePackage:}") String scanBasePackage) {
        this.scanBasePackage = scanBasePackage;
        this.exclusionDefaultGlobalException = exclusionDefaultGlobalException;
        init();
    }

    private void init() {
        Reflections reflections = new Reflections(scanBasePackage);
        Set<Class<?>> classes = reflections.getTypesAnnotatedWith(GlobalException.class);
        for (Class<?> clazz : classes) {
            if (verifyClassCanBeAdded(clazz)) {
                continue;
            }
            addClassToMappedExceptions(clazz);
        }
        IGlobalException iGlobalException = mappedExceptions.get(Throwable.class);
        if (iGlobalException == null) {
            mappedExceptions.put(Exception.class, new GlobalExceptionProcess());
        }
    }

    /**
     * Add class to mappedExceptions.
     * If duplicate throwable exists during saving, which needs to be overwritten according to priority.
     *
     * @param clazz Class<? extends Throwable>
     */
    private void addClassToMappedExceptions(Class<?> clazz) {
        GlobalException currentAnno = clazz.getAnnotation(GlobalException.class);
        Class<? extends Throwable> throwable = currentAnno.throwable();
        IGlobalException sourceException = mappedExceptions.get(throwable);
        try {
            if (sourceException == null) {
                mappedExceptions.put(throwable, (IGlobalException) clazz.newInstance());
                return;
            }
            GlobalException sourceAnno = sourceException.getClass().getAnnotation(GlobalException.class);
            if (currentAnno.priority() < sourceAnno.priority()) {
                log.warn("The exception already exists, overwrite the original exception! exception type: {}", throwable);
                log.warn("Original exception info：{}, class name: {}", sourceAnno, sourceException.getClass().getName());
                log.warn("Current exception info：{}, class name: {}", currentAnno, clazz.getName());
                mappedExceptions.put(throwable, (IGlobalException) clazz.newInstance());
            } else {
                log.warn("The exception already exists and has a low priority. The original exception cannot be overwritten! exception type: {}", throwable);
                log.warn("Original exception info：{}, class name: {}", sourceAnno, sourceException.getClass().getName());
                log.warn("Current exception info：{}, class name: {}", currentAnno, clazz.getName());
            }
        } catch (Exception e) {
            log.error("", e);
        }
    }

    private boolean verifyClassCanBeAdded(Class<?> clazz) {
        if (!checkAssignableFrom(IGlobalException.class, clazz.getInterfaces())) {
            log.error("The IGlobalException interface is not implemented and cannot be added to mappedExceptions, class name: {}", clazz.getName());
            return true;
        }
        if (isExclusionDefaultGlobalException(clazz.getName())) {
            log.warn("Global exception handler{" + clazz.getName() + "} In the exclusion list, do not add.");
            return true;
        }
        return false;
    }

    private boolean checkAssignableFrom(Class<?> assignableClass, Class<?>... classes) {
        for (Class face : classes) {
            if (face.isAssignableFrom(assignableClass)) {
                return true;
            }
        }
        return false;
    }

    public IGlobalException getCacheException(Class<? extends Throwable> exceptionType) {
        IGlobalException globalException = this.exceptionLookupCache.get(exceptionType);
        if (globalException == null) {
            globalException = getMappedException(exceptionType);
            this.exceptionLookupCache.put(exceptionType, globalException);
        }
        return globalException;
    }

    private IGlobalException getMappedException(Class<? extends Throwable> exceptionType) {
        List<Class<? extends Throwable>> matches = new ArrayList<>();
        for (Class<? extends Throwable> mappedException : this.mappedExceptions.keySet()) {
            if (checkAssignableFrom(exceptionType, mappedException)) {
                matches.add(mappedException);
            }
        }
        if (!matches.isEmpty()) {
            if (matches.size() > 1) {
                matches.sort(new ExceptionDepthComparator(exceptionType));
            }
            return this.mappedExceptions.get(matches.get(0));
        }
        return null;
    }

    private boolean isExclusionDefaultGlobalException(String className) {
        if (exclusionDefaultGlobalException == null || exclusionDefaultGlobalException.isEmpty()) {
            return false;
        }
        for (String exclusionClassName : exclusionDefaultGlobalException) {
            if (exclusionClassName.equals(className)) {
                return true;
            }
        }
        return false;
    }

    @ExceptionHandler(value = Throwable.class)
    public Object exceptionHandler(Throwable throwable, HttpServletRequest request, HttpServletResponse response) {
        IGlobalException iGlobalException = getCacheException(throwable.getClass());
        return iGlobalException.exception(throwable, request, response);
    }


/*    @ExceptionHandler(DataIntegrityViolationException.class)
    @ResponseBody
    public ResponseEntity<R> handleSQLIntegrityConstraintViolationException(DataIntegrityViolationException ex, HttpServletRequest request, HttpServletResponse response) {
        if (ex.getMessage().contains("Duplicate entry") && ex.getMessage().contains("user.phone")) {
            return new ResponseEntity<>(R.Failed(MessageConstant.PHONE_EXISTS), HttpStatus.BAD_REQUEST);
        } else {
            return new ResponseEntity<>(R.Failed(MessageConstant.UNKNOWN_ERROR), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }*/
}
